/*
 * Copyright 2023 Salesforce, Inc. All rights reserved.
 * The software in this package is published under the terms of the CPAL v1.0
 * license, a copy of which has been included with this distribution in the
 * LICENSE.txt file.
 */
package org.mule.runtime.core.privileged.processor.chain;

import static org.mule.runtime.api.exception.ExceptionHelper.getRootMuleException;
import static org.mule.runtime.core.api.rx.Exceptions.unwrap;

import static java.util.function.Function.identity;
import static java.util.Optional.ofNullable;

import org.mule.runtime.api.component.Component;
import org.mule.runtime.api.exception.ComposedErrorException;
import org.mule.runtime.api.message.Error;
import org.mule.runtime.api.message.ErrorType;
import org.mule.runtime.core.api.event.CoreEvent;
import org.mule.runtime.core.api.execution.ExceptionContextProvider;
import org.mule.runtime.core.api.processor.Processor;
import org.mule.runtime.core.api.retry.policy.RetryPolicyExhaustedException;

import org.mule.runtime.core.privileged.exception.ErrorTypeLocator;
import org.mule.runtime.core.privileged.exception.EventProcessingException;

import org.mule.runtime.core.internal.exception.MessagingException;
import org.mule.runtime.core.internal.message.ErrorBuilder;
import org.mule.runtime.core.internal.util.MessagingExceptionResolver;

import java.util.Collection;
import java.util.function.BiFunction;
import java.util.function.Function;

/**
 * Provide standard methods to do error mapping/handling in a {@link MessageProcessorChain}.
 *
 * @since 4.3
 */
public final class ChainErrorHandlingUtils {

  private ChainErrorHandlingUtils() {
    // Nothing to do
  }

  /**
   * Used to catch exceptions emitted by reactor operators and wrap these in a MessagingException while conserving a reference to
   * the failed Event.
   */
  public static BiFunction<Throwable, Object, Throwable> getLocalOperatorErrorHook(Processor processor, ErrorTypeLocator locator,
                                                                                   Collection<ExceptionContextProvider> exceptionContextProviders) {
    final MessagingExceptionResolver exceptionResolver =
        (processor instanceof Component) ? new MessagingExceptionResolver((Component) processor) : null;
    final Function<MessagingException, MessagingException> messagingExceptionMapper =
        resolveMessagingException(processor, e -> exceptionResolver.resolve(e, locator, exceptionContextProviders));

    return (throwable, event) -> {
      throwable = unwrap(throwable);
      if (event instanceof CoreEvent) {
        if (throwable instanceof MessagingException) {
          return messagingExceptionMapper.apply((MessagingException) throwable);
        } else {
          return resolveException(processor, (CoreEvent) event, throwable, locator, exceptionContextProviders, exceptionResolver);
        }
      } else {
        return throwable;
      }
    };
  }

  static MessagingException resolveException(Processor processor, CoreEvent event, Throwable throwable,
                                             ErrorTypeLocator locator,
                                             Collection<ExceptionContextProvider> exceptionContextProviders,
                                             MessagingExceptionResolver exceptionResolver) {
    if (event.getError().isPresent()) {
      // Clear any current error from the event, so it doesn't contaminate the upcoming mapping
      event = CoreEvent.builder(event).error(null).build();
    }

    if (processor instanceof Component) {
      return exceptionResolver.resolve(new MessagingException(event, throwable, (Component) processor), locator,
                                       exceptionContextProviders);
    } else {
      return new MessagingException(event, throwable);
    }
  }

  static Function<MessagingException, MessagingException> resolveMessagingException(Processor processor,
                                                                                    Function<MessagingException, MessagingException> messagingExceptionMapper) {
    if (processor instanceof Component) {
      return exception -> {
        if (!exception.getEvent().getError().isPresent()
            // Certain errors generated by routers have to be always resolved
            || exception.getCause() instanceof RetryPolicyExhaustedException
            || exception.getCause() instanceof ComposedErrorException) {
          return messagingExceptionMapper.apply(exception);
        } else {
          return exception;
        }
      };
    } else {
      return identity();
    }
  }

  /**
   * Finds or Creates an {@link Error} based on a provided {@link EventProcessingException}.
   * 
   * @param exception        Provided {@link EventProcessingException}.
   * @param errorTypeLocator An error type locator that will be used to infer the {@link Error#getErrorType()} value if the
   *                         provided {@link EventProcessingException} does not declare it.
   * @return {@link Error} found at the provided exception's {@link CoreEvent} or created based on the exception data.
   * @see EventProcessingException#getEvent()
   */
  static Error resolveError(EventProcessingException exception, ErrorTypeLocator errorTypeLocator) {
    // Error type may not be resolved yet (for example, when the exception is coming from components that do not use
    // ModuleExceptions or when critical errors occur).
    ErrorType errorType = ofNullable(exception.getExceptionInfo().getErrorType())
        .orElse(errorTypeLocator.lookupErrorType(getRootMuleException(exception)));
    return exception.getEvent().getError().orElseGet(() -> ErrorBuilder.builder()
        .exception(exception)
        .description(exception.getMessage())
        .detailedDescription(exception.getDetailedMessage())
        .errorType(errorType)
        .failingComponent(exception.getFailingComponent())
        .build());
  }


}
